Skip to content

Conversation

@sacrosanctic
Copy link
Contributor

closes #1298

I added both an integrated viewer and an external link just to see how it would feel.

The UI is really rough right now, and had a lot of assist from LLM, so I'll need to clean it up.

@vercel
Copy link

vercel bot commented Feb 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 10, 2026 0:35am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 10, 2026 0:35am
npmx-lunaria Ignored Ignored Feb 10, 2026 0:35am

Request Review

@github-actions
Copy link

github-actions bot commented Feb 10, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
lunaria/files/en-GB.json Localization changed, will be marked as complete.
lunaria/files/en-US.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@codecov
Copy link

codecov bot commented Feb 10, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1575 1 1574 6
View the top 1 failed test(s) by shortest run time
test/unit/a11y-component-coverage.spec.ts > a11y component test coverage > should have accessibility tests for all components (or be explicitly skipped)
Stack Traces | 0.014s run time
AssertionError: Missing a11y tests for 2 component(s):
  - Storybook/FileTree.vue
  - Storybook/MobileTreeDrawer.vue

To fix: Add tests in test/nuxt/a11y.spec.ts or add to SKIPPED_COMPONENTS in test/unit/a11y-component-coverage.spec.ts with justification.: expected 2 to equal +0

- Expected
+ Received

- 0
+ 2

 ❯ test/unit/a11y-component-coverage.spec.ts:164:12
 ❯ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:145:14
 ❯ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:915:28
 ❯ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:1243:24
 ❯ runWithTimeout node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:1209:12
 ❯ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:1653:42
 ❯ Traces.$ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@.../dist/chunks/traces.CCmnQaNT.js:142:29
 ❯ trace node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@.../dist/chunks/test.B8ej_ZHS.js:239:23
 ❯ runTest node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:1653:17

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copy link
Collaborator

@JReinhold JReinhold left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool!

I definitely like the in-npmx navigator better, it's just better DX.

However I'm thorn between it being in the top vs on the side. Putting it in the top (next to "code") would make the navigation experience consistent, it does something very similar to the code view, so putting them together makes sense from a user-expectation standpoint.

However it also works similar to a playground, so having it in the side with the other playground links makes sense too. But if we build it as the in-npmx experience (which IMO we should), it would act differently from all the other playground entries, which AFAIK are all external links with target="_blank".

WDYT @danielroe?

query: { storyid: node.storyId },
}
}
// For directories - navigate to first story in that directory
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead we want to follow the existing navigation pattern in the Code view, where clicking a directory only expands it (and changes it icon) but doesn't select any stories - only navigating to a story actually selects it.
I know that Storybook itself does the latter, but here I think we want to be consistent with npmx instead.

return getFileIcon(node.name)
}
const { toggleDir, isExpanded, autoExpandAncestors } = useStoryTreeState(props.baseUrl)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think autoExpandAncestors is the right idea, but it's not active at the moment.

Comment on lines +380 to +382
<span v-if="currentStory.story?.tags" class="text-fg-subtle">
{{ currentStory.story.tags.join(', ') }}
</span>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think tags is valuable information here, in 99 % of the cases it's internal information.


const playgroundLinks = computed(() => [
...readmeData.value.playgroundLinks,
...(packageJson.value?.storybook
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages can have a storybook property without having a storybook.url property, so we need to check for that here instead. Storybook addons have metadata in the same storybook property, but with no url, eg. https://npmxdev-git-fork-sacrosanctic-story-viewer-npmx.vercel.app/package-code/@storybook/addon-docs/v/10.2.8/package.json#L118-L124

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know where I can get source the type definition for package.json#storybook and /index.json?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can find the index type here:
import { StoryIndex } from 'storybook/internal/types';

As for the package.json definition, AFAIC there aren't any, we don't type it ourselves
https://github.com/storybookjs/storybook/blob/d29589dfa45bcb218479b013334747ed4655b2b9/code/core/src/common/utils/get-storybook-refs.ts#L28

@sacrosanctic sacrosanctic marked this pull request as ready for review February 12, 2026 06:41
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a native Storybook viewer feature to npmx.dev. It adds support for displaying and navigating Storybook components directly within the platform by fetching Storybook index.json files, transforming them into a hierarchical file tree, and rendering stories in an isolated iframe. The implementation includes new page routes, UI components for desktop and mobile layouts, state management for directory expansion, TypeScript type definitions for Storybook data structures, utility functions for tree transformation and breadcrumb generation, and internationalization strings for the new interface.

Suggested labels

front

Suggested reviewers

  • danielroe
  • graphieros
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description references issue #1298 and accurately describes the changes: adding both an integrated viewer and external link for Storybook, with acknowledgement that UI needs cleanup.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #1298: detects storybook.url in package.json, provides external link button, fetches Storybook index.json, builds story tree, and renders stories via iframe.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #1298 objectives. Added components, utilities, types, translations, and UI enhancements all serve the core goal of implementing a native stories viewer with Storybook integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (4)
app/utils/storybook-tree.ts (1)

6-72: Consider splitting transformStorybookEntries into smaller helpers.

The function is doing directory creation, parent linking, and story insertion in one block. Extracting helpers (e.g., getOrCreateDir, insertStory) would keep it closer to the guideline target and make it easier to test. As per coding guidelines: "Keep functions focused and manageable (generally under 50 lines)".

app/components/Storybook/FileTree.vue (2)

58-65: Use colon syntax for UnoCSS icons.

The Storybook icon is using dash syntax; switch to colon syntax to align with the project’s icon convention.

Suggested fix
-    return 'i-vscode-icons-file-type-storybook'
+    return 'i-vscode-icons:file-type-storybook'
Based on learnings: In Vue components that use UnoCSS with the preset-icons collection, prefer colon-syntax for icons (e.g., i-carbon:checkmark) over the dash-separated form (i-carbon-checkmark).

70-70: Remove or use the unused autoExpandAncestors binding.

It is currently declared but not used; either drop it or call it when a story becomes active.

app/components/Storybook/MobileTreeDrawer.vue (1)

68-72: Use colon syntax for UnoCSS icons.

classicon="i-carbon-close" should use the colon syntax to match the repo’s icon convention.

Suggested fix
-          classicon="i-carbon-close"
+          classicon="i-carbon:close"
Based on learnings: In Vue components that use UnoCSS with the preset-icons collection, prefer colon-syntax for icons (e.g., i-carbon:checkmark) over the dash-separated form (i-carbon-checkmark).

Comment on lines +27 to +83
// Build route object for a story
function getStoryRoute(node: StorybookFileTree): RouteLocationRaw {
if (node.type === 'story') {
return {
name: 'stories',
params: { path: props.basePath },
query: { storyid: node.storyId },
}
}
// For directories - navigate to first story in that directory
if (node.type === 'directory') {
const firstStory = getFirstStoryInDirectory(node)
if (firstStory) {
return {
name: 'stories',
params: { path: props.basePath },
query: { storyid: firstStory.storyId },
}
}
}
return { name: 'stories', params: { path: props.basePath } }
}
// Get icon for story or directory
function getNodeIcon(node: StorybookFileTree): string {
if (node.type === 'directory') {
return isNodeActive(node)
? 'i-carbon:folder-open text-yellow-500'
: 'i-carbon:folder text-yellow-600'
}
if (node.storyId) {
// Try to get icon based on story file type if available
if (node.story?.importPath) {
return getFileIcon(node.story.importPath)
}
// Default story icon
return 'i-vscode-icons-file-type-storybook'
}
return getFileIcon(node.name)
}
const { toggleDir, isExpanded, autoExpandAncestors } = useStoryTreeState(props.baseUrl)
// Handle directory click - toggle expansion and navigate to first story
function handleDirectoryClick(node: StorybookFileTree) {
if (node.type !== 'directory') return
// Toggle directory expansion
toggleDir(node.path)
// Navigate to first story in directory (if available)
const route = getStoryRoute(node)
if (route.query?.storyid) {
navigateTo(route)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Locate and read the FileTree.vue file
find . -name "FileTree.vue" -type f

Repository: npmx-dev/npmx.dev

Length of output: 135


🏁 Script executed:

# Search for StorybookFileTree type definition
rg "type StorybookFileTree|interface StorybookFileTree" -A 10

Repository: npmx-dev/npmx.dev

Length of output: 646


🏁 Script executed:

# Search for RouteLocationRaw usage in the file
rg "RouteLocationRaw" -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 3420


🏁 Script executed:

# Check basePath type and how it's used in the component
rg "basePath" app/components/Storybook/FileTree.vue -B 1 -A 1

Repository: npmx-dev/npmx.dev

Length of output: 569


🏁 Script executed:

# Check if autoExpandAncestors is used elsewhere in the file
rg "autoExpandAncestors" app/components/Storybook/FileTree.vue

Repository: npmx-dev/npmx.dev

Length of output: 148


🏁 Script executed:

# Read the complete FileTree.vue file to see all content
cat -n app/components/Storybook/FileTree.vue

Repository: npmx-dev/npmx.dev

Length of output: 4985


🏁 Script executed:

# Find route definitions for 'stories'
rg "name.*stories|stories.*route" app/ -B 2 -A 5 --type ts --type vue

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check app.vue or router configuration
find app -name "*.ts" -o -name "app.vue" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 772


🏁 Script executed:

# Look for router/routes configuration
fd -e ts -e vue | xargs rg "stories.*path|path.*param" -B 2 -A 2 | head -50

Repository: npmx-dev/npmx.dev

Length of output: 1542


🏁 Script executed:

# Check the route definition more carefully
cat -n app/pages/package-stories/[...path].vue | head -20

Repository: npmx-dev/npmx.dev

Length of output: 670


🏁 Script executed:

# Look for TypeScript/ESLint configuration to understand what errors would be caught
find . -name "tsconfig.*" -o -name ".eslintrc*" -o -name "nuxt.config.*" | head -10

Repository: npmx-dev/npmx.dev

Length of output: 135


🏁 Script executed:

# Check what type vue-router expects for RouteLocationRaw with params.path
rg "definePageMeta|path.*param" app/pages/package-stories/[...path].vue -B 3 -A 3

Repository: npmx-dev/npmx.dev

Length of output: 244


🏁 Script executed:

# Verify if there's any CI output or type errors
fd -e log -e txt | xargs grep -l "TS2322\|storyid\|query" 2>/dev/null | head -5

Repository: npmx-dev/npmx.dev

Length of output: 78


🏁 Script executed:

# Check Code/FileTree.vue to see similar pattern
cat -n app/components/Code/FileTree.vue | grep -A 10 "getFileRoute"

Repository: npmx-dev/npmx.dev

Length of output: 970


🏁 Script executed:

# Check how route params are handled in Nuxt for :path+ pattern
rg "useRoute\|path.*param" app/pages/package-stories/[...path].vue -A 10 | head -30

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check if there are TypeScript strict mode settings
cat -n tsconfig.json | head -40

Repository: npmx-dev/npmx.dev

Length of output: 506


🏁 Script executed:

# Verify if autoExpandAncestors is used anywhere after the destructuring
sed -n '70,85p' app/components/Storybook/FileTree.vue

Repository: npmx-dev/npmx.dev

Length of output: 537


🏁 Script executed:

# Check the nuxt.config to understand strict type checking
cat -n nuxt.config.ts | head -50

Repository: npmx-dev/npmx.dev

Length of output: 1761


🏁 Script executed:

# Look for how other routes handle the path parameter with string[]
rg "params.*path" app/ -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 5930


🏁 Script executed:

# Check if there's a package.json with type checking scripts
cat -n package.json | grep -A 10 "scripts"

Repository: npmx-dev/npmx.dev

Length of output: 3127


🏁 Script executed:

# Verify the complete getStoryRoute function and understand the query issue
ast-grep --pattern 'function getStoryRoute($_) { $$$ }'

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Run type checking to see actual errors
pnpm test:types 2>&1 | grep -A 5 "FileTree.vue"

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check if there's a build or type error log
pnpm build 2>&1 | grep -i "filetree\|storyid\|query" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Verify the actual type definition for route query access
rg "RouteLocationRaw|RouteLocation" shared/types --type ts -A 5

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check if storyId being optional causes issues by looking at how it's used elsewhere
rg "storyId" app/components/Storybook/FileTree.vue -B 1 -A 1

Repository: npmx-dev/npmx.dev

Length of output: 676


🏁 Script executed:

# Verify router param types from vue-router documentation in the code
rg "path\+" app/pages -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 1122


🏁 Script executed:

# Check if there are actual TypeScript errors by looking at node_modules or error reports
ls -la .nuxt/ 2>/dev/null | head -10

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Let me check the exact issue with optional storyId and undefined query values
python3 << 'EOF'
# Analyzing the type issue
analysis = """
StorybookFileTree.storyId is optional (?: string)
When getStoryRoute() returns { query: { storyid: node.storyId } }
and node.storyId is undefined, we get { storyid: undefined }

In handleDirectoryClick at line 81:
if (route.query?.storyid) - this checks for the property's truthiness

But the real issue is at creation time - RouteLocationRaw doesn't 
guarantee query exists or its shape. When storyId is undefined,
the query object shouldn't include it at all.
"""
print(analysis)
EOF

Repository: npmx-dev/npmx.dev

Length of output: 524


🏁 Script executed:

# Check if there are any actual test failures or TS errors related to this
find . -name "*.vue" -path "*/Storybook/*" | xargs wc -l

Repository: npmx-dev/npmx.dev

Length of output: 169


🏁 Script executed:

# Let's verify if the query object with undefined values is actually a type error
cat > /tmp/test-type.ts << 'EOF'
import type { RouteLocationRaw } from 'vue-router'

// Simulating the issue
const storyId: string | undefined = undefined

// This is what the code does - passing undefined in query
const route1: RouteLocationRaw = {
  name: 'stories',
  params: { path: ['test'] as [string, ...string[]] },
  query: { storyid: storyId }, // storyId could be undefined
}

// This is the safer pattern
const route2: RouteLocationRaw = storyId ? {
  name: 'stories',
  params: { path: ['test'] as [string, ...string[]] },
  query: { storyid: storyId },
} : {
  name: 'stories',
  params: { path: ['test'] as [string, ...string[]] },
}

console.log(route1, route2)
EOF
cat /tmp/test-type.ts

Repository: npmx-dev/npmx.dev

Length of output: 705


🏁 Script executed:

# Check if autoExpandAncestors is used in template or elsewhere
sed -n '1,129p' app/components/Storybook/FileTree.vue | grep -i "autoexpand"

Repository: npmx-dev/npmx.dev

Length of output: 148


🏁 Script executed:

# Verify that passing basePath directly (without .join) works elsewhere
rg "params.*path.*basePath" app/ -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 1415


🏁 Script executed:

# Verify what the 'stories' route expects for path parameter
rg "const.*path.*route\.params" app/pages/package-stories/[...path].vue -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check if basePath needs type assertion like Code components do
rg "string\[\].*RouteLocationRaw\|RouteLocationRaw.*string\[\]" app/components -B 3 -A 3

Repository: npmx-dev/npmx.dev

Length of output: 43


Address optional storyId when building route objects.

StorybookFileTree.storyId is optional, so getStoryRoute() can return query objects with undefined values. Only include the query property when a story ID exists. Similarly, remove the unused autoExpandAncestors from the destructuring on line 70.

Suggested fix
 // Build route object for a story
 function getStoryRoute(node: StorybookFileTree): RouteLocationRaw {
   if (node.type === 'story') {
+    if (node.storyId) {
       return {
         name: 'stories',
         params: { path: props.basePath },
         query: { storyid: node.storyId },
       }
+    }
   }
   // For directories - navigate to first story in that directory
   if (node.type === 'directory') {
     const firstStory = getFirstStoryInDirectory(node)
-    if (firstStory) {
+    if (firstStory?.storyId) {
       return {
         name: 'stories',
         params: { path: props.basePath },
         query: { storyid: firstStory.storyId },
       }
     }
   }
   return { name: 'stories', params: { path: props.basePath } }
 }

-const { toggleDir, isExpanded, autoExpandAncestors } = useStoryTreeState(props.baseUrl)
+const { toggleDir, isExpanded } = useStoryTreeState(props.baseUrl)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Build route object for a story
function getStoryRoute(node: StorybookFileTree): RouteLocationRaw {
if (node.type === 'story') {
return {
name: 'stories',
params: { path: props.basePath },
query: { storyid: node.storyId },
}
}
// For directories - navigate to first story in that directory
if (node.type === 'directory') {
const firstStory = getFirstStoryInDirectory(node)
if (firstStory) {
return {
name: 'stories',
params: { path: props.basePath },
query: { storyid: firstStory.storyId },
}
}
}
return { name: 'stories', params: { path: props.basePath } }
}
// Get icon for story or directory
function getNodeIcon(node: StorybookFileTree): string {
if (node.type === 'directory') {
return isNodeActive(node)
? 'i-carbon:folder-open text-yellow-500'
: 'i-carbon:folder text-yellow-600'
}
if (node.storyId) {
// Try to get icon based on story file type if available
if (node.story?.importPath) {
return getFileIcon(node.story.importPath)
}
// Default story icon
return 'i-vscode-icons-file-type-storybook'
}
return getFileIcon(node.name)
}
const { toggleDir, isExpanded, autoExpandAncestors } = useStoryTreeState(props.baseUrl)
// Handle directory click - toggle expansion and navigate to first story
function handleDirectoryClick(node: StorybookFileTree) {
if (node.type !== 'directory') return
// Toggle directory expansion
toggleDir(node.path)
// Navigate to first story in directory (if available)
const route = getStoryRoute(node)
if (route.query?.storyid) {
navigateTo(route)
}
// Build route object for a story
function getStoryRoute(node: StorybookFileTree): RouteLocationRaw {
if (node.type === 'story') {
if (node.storyId) {
return {
name: 'stories',
params: { path: props.basePath },
query: { storyid: node.storyId },
}
}
}
// For directories - navigate to first story in that directory
if (node.type === 'directory') {
const firstStory = getFirstStoryInDirectory(node)
if (firstStory?.storyId) {
return {
name: 'stories',
params: { path: props.basePath },
query: { storyid: firstStory.storyId },
}
}
}
return { name: 'stories', params: { path: props.basePath } }
}
// Get icon for story or directory
function getNodeIcon(node: StorybookFileTree): string {
if (node.type === 'directory') {
return isNodeActive(node)
? 'i-carbon:folder-open text-yellow-500'
: 'i-carbon:folder text-yellow-600'
}
if (node.storyId) {
// Try to get icon based on story file type if available
if (node.story?.importPath) {
return getFileIcon(node.story.importPath)
}
// Default story icon
return 'i-vscode-icons-file-type-storybook'
}
return getFileIcon(node.name)
}
const { toggleDir, isExpanded } = useStoryTreeState(props.baseUrl)
// Handle directory click - toggle expansion and navigate to first story
function handleDirectoryClick(node: StorybookFileTree) {
if (node.type !== 'directory') return
// Toggle directory expansion
toggleDir(node.path)
// Navigate to first story in directory (if available)
const route = getStoryRoute(node)
if (route.query?.storyid) {
navigateTo(route)
}
🧰 Tools
🪛 GitHub Actions: ci

[error] 30-30: TS2322: Type '{ name: "stories"; params: { path: string[]; }; query: { storyid: string | undefined; }; }' is not assignable to type 'RouteLocationRaw'.

🪛 GitHub Check: 💪 Type check

[failure] 81-81:
Property 'query' does not exist on type 'RouteLocationRaw'.


[failure] 70-70:
'autoExpandAncestors' is declared but its value is never read.


[failure] 47-47:
Type '{ name: "stories"; params: { path: string[]; }; }' is not assignable to type 'RouteLocationRaw'.


[failure] 40-40:
Type '{ name: "stories"; params: { path: string[]; }; query: { storyid: string | undefined; }; }' is not assignable to type 'RouteLocationRaw'.


[failure] 30-30:
Type '{ name: "stories"; params: { path: string[]; }; query: { storyid: string | undefined; }; }' is not assignable to type 'RouteLocationRaw'.

Comment on lines +59 to +77
// Fetch package.json to get Storybook URL
const { data: packageJson } = useFetch<{ storybook: { title: string; url: string } }>(() => {
const url = `https://cdn.jsdelivr.net/npm/${packageName.value}/package.json`
return url
})
// Fetch Storybook index data
const { data: storybookData, status: storybookStatus } = useLazyFetch<{
v: number
entries: Record<string, StorybookEntry>
}>(
() => {
if (!packageJson.value?.storybook?.url) return ''
return packageJson.value.storybook.url + '/index.json'
},
{
key: computed(() => `storybook:${packageName.value}`),
server: false, // Storybook URLs are usually client-side only
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the file
fd -t f "package-stories" -p --exec cat {} \;

Repository: npmx-dev/npmx.dev

Length of output: 15214


🏁 Script executed:

# Let's look at the exact file path
git ls-files | grep package-stories

Repository: npmx-dev/npmx.dev

Length of output: 100


🏁 Script executed:

# Read the specific file with line numbers
cat -n app/pages/package-stories/[...path].vue | head -100

Repository: npmx-dev/npmx.dev

Length of output: 4121


Include version in both Storybook fetch URL and cache key.

The CDN URL and lazy fetch cache key ignore the selected version, causing the component to always fetch the latest package.json and reuse Storybook data when switching versions. This results in mismatched metadata for the selected version. Include the version in the URL and fetch key, and guard the fetch to avoid running until version is available.

Suggested fix
-const { data: packageJson } = useFetch<{ storybook: { title: string; url: string } }>(() => {
-  const url = `https://cdn.jsdelivr.net/npm/${packageName.value}/package.json`
-  return url
-})
+const { data: packageJson } = useFetch<{ storybook?: { title: string; url: string } }>(() => {
+  if (!version.value) return null
+  return `https://cdn.jsdelivr.net/npm/${packageName.value}@${version.value}/package.json`
+})
-  () => {
-    if (!packageJson.value?.storybook?.url) return ''
-    return packageJson.value.storybook.url + '/index.json'
-  },
+  () => {
+    if (!packageJson.value?.storybook?.url || !version.value) return ''
+    return `${packageJson.value.storybook.url}/index.json`
+  },
   {
-    key: computed(() => `storybook:${packageName.value}`),
+    key: computed(() => `storybook:${packageName.value}@${version.value}`),
     server: false,
   },
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Fetch package.json to get Storybook URL
const { data: packageJson } = useFetch<{ storybook: { title: string; url: string } }>(() => {
const url = `https://cdn.jsdelivr.net/npm/${packageName.value}/package.json`
return url
})
// Fetch Storybook index data
const { data: storybookData, status: storybookStatus } = useLazyFetch<{
v: number
entries: Record<string, StorybookEntry>
}>(
() => {
if (!packageJson.value?.storybook?.url) return ''
return packageJson.value.storybook.url + '/index.json'
},
{
key: computed(() => `storybook:${packageName.value}`),
server: false, // Storybook URLs are usually client-side only
},
// Fetch package.json to get Storybook URL
const { data: packageJson } = useFetch<{ storybook?: { title: string; url: string } }>(() => {
if (!version.value) return null
return `https://cdn.jsdelivr.net/npm/${packageName.value}@${version.value}/package.json`
})
// Fetch Storybook index data
const { data: storybookData, status: storybookStatus } = useLazyFetch<{
v: number
entries: Record<string, StorybookEntry>
}>(
() => {
if (!packageJson.value?.storybook?.url || !version.value) return ''
return `${packageJson.value.storybook.url}/index.json`
},
{
key: computed(() => `storybook:${packageName.value}@${version.value}`),
server: false, // Storybook URLs are usually client-side only
},

Comment on lines +105 to +116
// Auto-select first story if none specified
watch(
[storybookTree, () => parsedRoute.value.storyId],
([tree, storyId]) => {
if (tree.length && !storyId) {
const first = getFirstStory(tree)
if (first?.storyId) {
navigateTo({
name: 'stories',
params: { path: [...packageName.value.split('/'), 'v', version.value!] },
query: { storyid: first.storyId },
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "*.vue" -path "*package-stories*" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 102


🏁 Script executed:

cat -n ./app/pages/package-stories/[...path].vue

Repository: npmx-dev/npmx.dev

Length of output: 18210


🏁 Script executed:

find . -name "*.ts" -o -name "*.d.ts" | xargs grep -l "stories.*route\|path\+" | head -10

Repository: npmx-dev/npmx.dev

Length of output: 443


🏁 Script executed:

find . -name "*.config.*" -o -name "*routes*" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 318


🏁 Script executed:

cat -n ./nuxt.config.ts | head -100

Repository: npmx-dev/npmx.dev

Length of output: 3293


🏁 Script executed:

find . -name "*.d.ts" -path "*/app/*" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.d.ts" \) -exec grep -l "definePageMeta\|useRoute.*stories" {} \; | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

find . -name "tsconfig.json" -o -name "tsconfig*.json"

Repository: npmx-dev/npmx.dev

Length of output: 96


🏁 Script executed:

cat -n ./tsconfig.json | head -80

Repository: npmx-dev/npmx.dev

Length of output: 506


🏁 Script executed:

grep -r "useRoute.*stories\|routes.*stories\|path.*\+.*path" . --include="*.ts" --include="*.vue" --include="*.d.ts" 2>/dev/null | head -20

Repository: npmx-dev/npmx.dev

Length of output: 1937


🏁 Script executed:

cat -n ./app/pages/package-docs/[...path].vue | head -100

Repository: npmx-dev/npmx.dev

Length of output: 3751


🏁 Script executed:

cat -n ./app/pages/package-code/[...path].vue | head -100

Repository: npmx-dev/npmx.dev

Length of output: 4424


🏁 Script executed:

grep -A 5 "navigateTo" ./app/pages/package-code/[...path].vue | head -30

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

grep -n "navigateTo\|as \[string" ./app/pages/package-docs/[...path].vue

Repository: npmx-dev/npmx.dev

Length of output: 266


🏁 Script executed:

grep -B 2 -A 2 "as \[string" ./app/pages/package-docs/[...path].vue ./app/components/Code/DirectoryListing.vue ./app/components/Code/FileTree.vue

Repository: npmx-dev/npmx.dev

Length of output: 1870


🏁 Script executed:

cd ./app/pages/package-stories && ls -la

Repository: npmx-dev/npmx.dev

Length of output: 238


Add storyId guard at line 162 and apply type casting pattern for route params.

Line 162 is missing a storyId guard: change if (firstStoryInCategory) to if (firstStoryInCategory?.storyId) to prevent undefined errors when navigating. Additionally, the navigateTo params at lines 114, 165, and 178 should be typed consistently with other pages in the codebase (package-docs, package-code) using as [string, ...string[]] rather than .join('/').

Suggested fixes

Line 162:

-          if (firstStoryInCategory) {
+          if (firstStoryInCategory?.storyId) {

Lines 114, 165, 178 (apply type casting):

-            params: { path: [...packageName.value.split('/'), 'v', version.value!] },
+            params: { path: [...packageName.value.split('/'), 'v', version.value!] as [string, ...string[]] },
-              params: { path: [...packageName.value.split('/'), 'v', newVersion] },
+              params: { path: [...packageName.value.split('/'), 'v', newVersion] as [string, ...string[]] },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Auto-select first story if none specified
watch(
[storybookTree, () => parsedRoute.value.storyId],
([tree, storyId]) => {
if (tree.length && !storyId) {
const first = getFirstStory(tree)
if (first?.storyId) {
navigateTo({
name: 'stories',
params: { path: [...packageName.value.split('/'), 'v', version.value!] },
query: { storyid: first.storyId },
})
// Auto-select first story if none specified
watch(
[storybookTree, () => parsedRoute.value.storyId],
([tree, storyId]) => {
if (tree.length && !storyId) {
const first = getFirstStory(tree)
if (first?.storyId) {
navigateTo({
name: 'stories',
params: { path: [...packageName.value.split('/'), 'v', version.value!] as [string, ...string[]] },
query: { storyid: first.storyId },
})
🧰 Tools
🪛 GitHub Check: 💪 Type check

[failure] 112-112:
Argument of type '{ name: "stories"; params: { path: [...string[], string, string]; }; query: { storyid: string; }; }' is not assignable to parameter of type 'RouteLocationRaw | null | undefined'.

Comment on lines +192 to +196
// Navigation helper - build URL for a story
function getStoryUrl(story: StorybookFileTree): string {
const base = `/package-stories/${packageName.value}/v/${version.value}`
return `${base}?storyid=${story.storyId}`
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Breadcrumb links can produce ?storyid=undefined.

getStoryUrl always appends storyId, but intermediate crumbs don’t have one, so their links become invalid and prevent auto-selection. Consider only appending the query when a story id is present (or resolve a valid story id per crumb).

Suggested fix
 function getStoryUrl(story: StorybookFileTree): string {
   const base = `/package-stories/${packageName.value}/v/${version.value}`
-  return `${base}?storyid=${story.storyId}`
+  return story.storyId ? `${base}?storyid=${story.storyId}` : base
 }

Also applies to: 295-306

Comment on lines +73 to +85
const playgroundLinks = computed(() => [
...readmeData.value.playgroundLinks,
...(packageJson.value?.storybook
? [
{
url: packageJson.value.storybook.url,
provider: 'storybook',
providerName: 'Storybook',
label: 'Storybook',
},
]
: []),
])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect Storybook link rendering and any URL sanitisation.

fd -a 'PackagePlaygrounds\.vue$' app | xargs -I{} sed -n '1,220p' {}
fd -a 'LinkBase\.vue$' app | xargs -I{} sed -n '1,220p' {}
rg -n "sanitize|safeUrl|safe|http" app/components app/utils

Repository: npmx-dev/npmx.dev

Length of output: 8953


🏁 Script executed:

#!/bin/bash
# Get the full LinkBase.vue file
cat -n app/components/Link/Base.vue

# Find PackagePlaygrounds
fd -i 'playground' app/components --type f

Repository: npmx-dev/npmx.dev

Length of output: 5095


🏁 Script executed:

#!/bin/bash
# Read the Playgrounds component
cat -n app/components/Package/Playgrounds.vue

Repository: npmx-dev/npmx.dev

Length of output: 8282


Validate the Storybook URL before surfacing it.

package.json is untrusted input. The storybook.url is rendered directly in the Playgrounds component without any validation. If the URL property is missing, has a non-string value, or uses a non-http(s) scheme, you could emit a broken or unsafe link. LinkBase.vue's URL validation is not used here (raw <a> tags are used instead).

🔒 Suggested guard
-const playgroundLinks = computed(() => [
-  ...readmeData.value.playgroundLinks,
-  ...(packageJson.value?.storybook
-    ? [
-        {
-          url: packageJson.value.storybook.url,
-          provider: 'storybook',
-          providerName: 'Storybook',
-          label: 'Storybook',
-        },
-      ]
-    : []),
-])
+const playgroundLinks = computed(() => {
+  const storybook = packageJson.value?.storybook
+  const storybookUrl =
+    storybook && typeof storybook.url === 'string' && /^https?:\/\//i.test(storybook.url)
+      ? storybook.url
+      : null
+
+  return [
+    ...readmeData.value.playgroundLinks,
+    ...(storybookUrl
+      ? [
+          {
+            url: storybookUrl,
+            provider: 'storybook',
+            providerName: 'Storybook',
+            label: 'Storybook',
+          },
+        ]
+      : []),
+  ]
+})

Comment on lines +654 to +662
<LinkBase
v-if="packageJson?.storybook"
variant="button-secondary"
:to="{ name: 'stories', params: { path: [pkg.name, 'v', resolvedVersion] } }"
aria-keyshortcuts="."
classicon="i-carbon:code"
>
stories
</LinkBase>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "package\.links\.stories" app

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

rg -n "package\.links\." app/locales --max-count=20

Repository: npmx-dev/npmx.dev

Length of output: 112


🏁 Script executed:

fd -e vue app/pages && cat -n app/pages/package/\[\[org\]\]/\[name\].vue | sed -n '654,680p'

Repository: npmx-dev/npmx.dev

Length of output: 402


🏁 Script executed:

git ls-files | grep -E "(locale|i18n|translation)" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 529


🏁 Script executed:

git ls-files | grep -E "\.vue$" | grep -E "package.*\[org\].*\[name\]"

Repository: npmx-dev/npmx.dev

Length of output: 189


🏁 Script executed:

fd -type d -name "locales" -o -name "lang" -o -name "i18n"

Repository: npmx-dev/npmx.dev

Length of output: 231


🏁 Script executed:

cat -n app/pages/package/\[\[org\]\]/\[name\].vue | sed -n '654,680p'

Repository: npmx-dev/npmx.dev

Length of output: 1235


🏁 Script executed:

rg -n "package\.links" i18n/locales/en-GB.json

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

rg -n "stories" i18n/locales/en-GB.json

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

cat i18n/locales/en-GB.json | head -200

Repository: npmx-dev/npmx.dev

Length of output: 1674


🏁 Script executed:

cat -n app/pages/package/\[\[org\]\]/\[name\].vue | sed -n '620,700p'

Repository: npmx-dev/npmx.dev

Length of output: 3695


🏁 Script executed:

rg -n "package\." i18n/locales/en-GB.json | head -50

Repository: npmx-dev/npmx.dev

Length of output: 294


🏁 Script executed:

python3 << 'EOF'
import json

# Read the en-GB locale file
with open('i18n/locales/en-GB.json', 'r') as f:
    data = json.load(f)

# Navigate to package.links if it exists
if 'package' in data and 'links' in data['package']:
    print("package.links keys found:")
    for key in data['package']['links']:
        print(f"  - {key}: {data['package']['links'][key]}")
else:
    print("package.links structure:")
    if 'package' in data:
        print(f"  Keys in package: {list(data['package'].keys())}")
    else:
        print("  No 'package' key found in en-GB.json")
EOF

Repository: npmx-dev/npmx.dev

Length of output: 124


🏁 Script executed:

cat i18n/locales/en.json | python3 -m json.tool | grep -A 20 '"package"'

Repository: npmx-dev/npmx.dev

Length of output: 3465


🏁 Script executed:

cat i18n/locales/en.json | python3 -m json.tool | grep -A 50 '"links"'

Repository: npmx-dev/npmx.dev

Length of output: 4150


🏁 Script executed:

cat i18n/locales/en.json | python3 << 'EOF'
import json
import sys

data = json.load(sys.stdin)

# Navigate through the structure
if 'package' in data:
    print("Found 'package' key")
    if 'links' in data['package']:
        print("Found 'package.links':")
        print(json.dumps(data['package']['links'], indent=2))
    else:
        print("No 'package.links' found")
        print("Available keys in package:", list(data['package'].keys()))
EOF

Repository: npmx-dev/npmx.dev

Length of output: 781


🏁 Script executed:

rg -n "docs|code|compare" i18n/locales/en.json | head -20

Repository: npmx-dev/npmx.dev

Length of output: 1118


Localise the label, fix the duplicate shortcut, and consider the icon.

The button text is hardcoded, and aria-keyshortcuts="." conflicts with the Code button. Additionally, package.links.stories does not exist in the locale files, so the i18n key would need to be added to all locale files alongside the localisation update. Consider also changing the icon from i-carbon:code to avoid duplication with the Code button.

✍️ Suggested tweak
-              aria-keyshortcuts="."
-              classicon="i-carbon:code"
+              classicon="i-carbon:code"
             >
-              stories
+              {{ $t('package.links.stories') }}
             </LinkBase>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<LinkBase
v-if="packageJson?.storybook"
variant="button-secondary"
:to="{ name: 'stories', params: { path: [pkg.name, 'v', resolvedVersion] } }"
aria-keyshortcuts="."
classicon="i-carbon:code"
>
stories
</LinkBase>
<LinkBase
v-if="packageJson?.storybook"
variant="button-secondary"
:to="{ name: 'stories', params: { path: [pkg.name, 'v', resolvedVersion] } }"
classicon="i-carbon:code"
>
{{ $t('package.links.stories') }}
</LinkBase>

Comment on lines +50 to +57
// Create story node
const storyNode: StorybookFileTree = {
name: storyName,
path: entry.title,
type: 'story',
storyId: entry.id,
story: entry,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure story nodes have unique paths.

path: entry.title collides with the directory path and with other stories that share the same title, which can cause duplicate keys and unstable tree navigation. Include the story name (or id) in the path to make each story node unique.

Suggested fix
-    const storyNode: StorybookFileTree = {
-      name: storyName,
-      path: entry.title,
+    const storyNode: StorybookFileTree = {
+      name: storyName,
+      path: `${entry.title}/${storyName}`,
       type: 'story',
       storyId: entry.id,
       story: entry,
     }

Comment on lines +23 to +27
story?: string
/** Story parameters and configuration */
parameters?: Record<string, any>
/** Story metadata */
type?: 'story' | 'docs' | 'component'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n shared/types/storybook.ts | head -30

Repository: npmx-dev/npmx.dev

Length of output: 1064


🌐 Web query:

TypeScript best practices anyvsunknown type safety

💡 Result:

any vs unknown: what to prefer (and why)

  • any = “turn off the checker here”. You can access any properties, call it, assign it anywhere, and TypeScript won’t protect you—errors can slip to runtime. any also tends to propagate (“contagious”) through your code. [1]
  • unknown = “I don’t know yet, force me to prove it”. Anything can be assigned to unknown, but you can’t use it (property access/calls, etc.) until you narrow it (type guards, instanceof, typeof, schema validation, etc.). It’s explicitly described as the type-safe counterpart of any. [2]

Best-practice guidance

  1. Default to unknown for untrusted/unknown data (JSON, user input, external APIs, catch errors, etc.), then narrow:

    function parse(input: string): unknown {
      return JSON.parse(input);
    }
    
    const v = parse("{}");     // unknown
    // v.x;                    // error
    if (typeof v === "object" && v !== null && "x" in v) {
      // now you can refine further
    }

    Rationale: unknown blocks unsafe usage until you validate. [2]

  2. Use any only as a last-resort escape hatch, and keep it localized (one boundary), immediately converting it to something safer (unknown + narrowing, or a real type). TypeScript’s own guidance calls out the type-safety tradeoff. [1]

  3. Turn on strictness + stop any from creeping in:

    • Enable "strict": true / at least "noImplicitAny": true to catch implicit anys. (Note: it doesn’t prevent explicit any.) [5]
    • Add lint rules like @typescript-eslint/no-explicit-any, which explicitly recommends unknown as the safer alternative and can even auto-suggest switching. [4]
  4. Handle exceptions safely:

    • Prefer unknown in catch variables via useUnknownInCatchVariables, so you must verify what was thrown before using it (e.g., err instanceof Error). [3]

Practical rule of thumb

  • If you truly don’t know the type yet: unknown + narrow. [2]
  • If you know the type: model it (interface/type/generics) instead of either. [4]
  • If you must bypass the type system temporarily: use any, but isolate it and replace it ASAP. [1][4]

Sources: TypeScript docs on any and unknown [1][2], useUnknownInCatchVariables [3], and typescript-eslint guidance on avoiding any/preferring unknown [4][5].


Replace Record<string, any> with Record<string, unknown> for type safety.

The parameters field accepts external JSON data from Storybook's index, making it the exact scenario where unknown is preferred over any. Using unknown enforces that consumers validate the structure before access, preventing type-safety violations. This aligns with both TypeScript best practices and the coding guideline requiring strict type-safe code.

🔧 Suggested fix
-  parameters?: Record<string, any>
+  parameters?: Record<string, unknown>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
story?: string
/** Story parameters and configuration */
parameters?: Record<string, any>
/** Story metadata */
type?: 'story' | 'docs' | 'component'
story?: string
/** Story parameters and configuration */
parameters?: Record<string, unknown>
/** Story metadata */
type?: 'story' | 'docs' | 'component'

@yannbf
Copy link

yannbf commented Feb 12, 2026

This is incredible!! I was testing out in a project and noticed that the MDX icon works for some entries and not for others, e.g. both of these should have the mdx icon:

https://npmxdev-git-fork-sacrosanctic-story-viewer-npmx.vercel.app/package-stories/@storybook/design-system/v/7.15.19?storyid=typography-base--docs

https://npmxdev-git-fork-sacrosanctic-story-viewer-npmx.vercel.app/package-stories/@storybook/design-system/v/7.15.19?storyid=typography-styles--docs

They're both type: "docs" in the index.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native stories viewer

3 participants